@Martin Konecny's answer provides an effective solution, but - as he mentions - it only works if the actual script is not invoked through a symlink residing in a different directory.
This answer covers that case: a solution that also works when the script is invoked through a symlink or even a chain of symlinks:
Linux / GNU readlink solution:
If your script needs to run on Linux only or you know that GNU readlink is in the $PATH, use readlink -f, which conveniently resolves a symlink to its ultimate target:
 scriptDir=$(dirname -- "$(readlink -f -- "$BASH_SOURCE")")
Note that GNU readlink has 3 related options for resolving a symlink to its ultimate target's full path: -f (--canonicalize), -e (--canonicalize-existing), and -m (--canonicalize-missing) - see man readlink.
Since the target by definition exists in this scenario, any of the 3 options can be used; I've chosen -f here, because it is the most well-known one.
Multi-(Unix-like-)platform solution (including platforms with a POSIX-only set of utilities):
If your script must run on any platform that:
- has a - readlinkutility, but lacks the- -foption (in the GNU sense of resolving a symlink to its ultimate target) - e.g., macOS.
 - 
- macOS uses an older version of the BSD implementation of readlink; note that recent versions of FreeBSD/PC-BSD do support-f.
 
- does not even have - readlink, but has POSIX-compatible utilities - e.g., HP-UX (thanks, @Charles Duffy).
 
The following solution, inspired by https://stackoverflow.com/a/1116890/45375,
defines helper shell function, rreadlink(), which resolves a given symlink to its ultimate target in a loop - this function is in effect a POSIX-compliant implementation of GNU readlink's -e option, which is similar to the -f option, except that the ultimate target must exist.
Note: The function is a bash function, and is POSIX-compliant only in the sense that only POSIX utilities with POSIX-compliant options are used. For a version of this function that is itself written in POSIX-compliant shell code (for /bin/sh), see here.
- If - readlinkis available, it is used (without options) - true on most modern platforms.
 
- Otherwise, the output from - ls -lis parsed, which is the only POSIX-compliant way to determine a symlink's target.
 Caveat: this will break if a filename or path contains the literal substring- ->- which is unlikely, however.
 (Note that platforms that lack- readlinkmay still provide other, non-POSIX methods for resolving a symlink; e.g., @Charles Duffy mentions HP-UX's- findutility supporting the- %lformat char. with its- -printfprimary; in the interest of brevity the function does NOT try to detect such cases.)
 
- An installable utility (script) form of the function below (with additional functionality) can be found as  - rreadlinkin the npm registry; on Linux and macOS, install it with- [sudo] npm install -g rreadlink; on other platforms (assuming they have- bash), follow the manual installation instructions.
 
If the argument is a symlink, the ultimate target's canonical path is returned; otherwise, the argument's own canonical path is returned.
#!/usr/bin/env bash
# Helper function.
rreadlink() ( # execute function in a *subshell* to localize the effect of `cd`, ...
  local target=$1 fname targetDir readlinkexe=$(command -v readlink) CDPATH= 
  # Since we'll be using `command` below for a predictable execution
  # environment, we make sure that it has its original meaning.
  { \unalias command; \unset -f command; } &>/dev/null
  while :; do # Resolve potential symlinks until the ultimate target is found.
      [[ -L $target || -e $target ]] || { command printf '%s\n' "$FUNCNAME: ERROR: '$target' does not exist." >&2; return 1; }
      command cd "$(command dirname -- "$target")" # Change to target dir; necessary for correct resolution of target path.
      fname=$(command basename -- "$target") # Extract filename.
      [[ $fname == '/' ]] && fname='' # !! curiously, `basename /` returns '/'
      if [[ -L $fname ]]; then
        # Extract [next] target path, which is defined
        # relative to the symlink's own directory.
        if [[ -n $readlinkexe ]]; then # Use `readlink`.
          target=$("$readlinkexe" -- "$fname")
        else # `readlink` utility not available.
          # Parse `ls -l` output, which, unfortunately, is the only POSIX-compliant 
          # way to determine a symlink's target. Hypothetically, this can break with
          # filenames containig literal ' -> ' and embedded newlines.
          target=$(command ls -l -- "$fname")
          target=${target#* -> }
        fi
        continue # Resolve [next] symlink target.
      fi
      break # Ultimate target reached.
  done
  targetDir=$(command pwd -P) # Get canonical dir. path
  # Output the ultimate target's canonical path.
  # Note that we manually resolve paths ending in /. and /.. to make sure we
  # have a normalized path.
  if [[ $fname == '.' ]]; then
    command printf '%s\n' "${targetDir%/}"
  elif  [[ $fname == '..' ]]; then
    # Caveat: something like /var/.. will resolve to /private (assuming
    # /var@ -> /private/var), i.e. the '..' is applied AFTER canonicalization.
    command printf '%s\n' "$(command dirname -- "${targetDir}")"
  else
    command printf '%s\n' "${targetDir%/}/$fname"
  fi
)
                
# Determine ultimate script dir. using the helper function.
# Note that the helper function returns a canonical path.
scriptDir=$(dirname -- "$(rreadlink "$BASH_SOURCE")")